/*
 *  Copyright (C) 2011 Jaime Pavlich-Mariscal
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package cl.ucn.disc.biblio.refcluster.reference;

import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import cl.ucn.disc.biblio.refcluster.control.ReferenceClustererController;
import cl.ucn.disc.biblio.refcluster.database.TupleOfStrings;
import cl.ucn.disc.biblio.refcluster.util.MultiCounter;
import cl.ucn.disc.biblio.refcluster.util.OrderedMultiMap;
import cl.ucn.disc.biblio.refcluster.util.TextTransformException;
import cl.ucn.disc.biblio.refcluster.util.TextTransformer;
import cl.ucn.disc.biblio.refcluster.util.Utils;

/** Miscelaneous operations that can be performed over bibliographic references.
 * @see Reference
 * @author Jaime Pavlich
 *
 */
public class ReferenceManager {

	private static final String TMP_EXTENSION = ".tmp";
	public static final Logger LOGGER = Logger.getLogger(ReferenceClustererController.class.getName());
	private FileFilter wosFilesFilter = new FileFilter() {

		@Override
		public boolean accept(File pathname) {
			String filename = pathname.getName().toLowerCase();
			return filename.endsWith(".txt");
		}
	};

	/**
	 * Reads a list of references from a file in WoS format. It only reads those references
	 * that appear after the {@code CR} keyword in the WoS file.
	 * 
	 * @param wosFile
	 * @return
	 * @throws FileNotFoundException
	 */
	public Set<Reference> readRefsFromWoSFile(File wosFile) throws FileNotFoundException {
		Scanner s = new Scanner(wosFile);

		Set<Reference> refs = new LinkedHashSet<Reference>();
		
		Iterator<BiblioRecord> it = BiblioRecord.getIterator(s);
		
		while (it.hasNext()) {
			Set<String> referenceStrings = it.next().getField(BiblioRecord.REFERENCES);
			for (String referenceString : referenceStrings) {
				try {
					refs.add(new Reference(referenceString));
				} catch (ParseException e) {
//					LOGGER.info(e + ": "  + referenceString);
				}
			}
			
			
		}
//		boolean addingRefs = false;
//
//		while (s.hasNextLine()) {
//			String line = s.nextLine();
//			if (line.trim().endsWith("DOI")) { // Concatenates the DOI if it is
//				// in the next line
//				line = line.concat(s.nextLine());
//			}
//
//			try {
//				if (line.startsWith(BiblioRecord.REFERENCES)) {
//					addingRefs = true;
//				} else if (addingRefs && line.startsWith(" ")) {
//					refs.add(new Reference(line.substring(3)));
//					refs.add(new Reference(line.substring(3)));
//				} else {
//					addingRefs = false;
//				}
//			} catch (Exception e) {
//				// do nothing
//			}
//
//		}

		return refs;

	}
	

	/** Executes {@link #readRefsFromWoSFile(File)} for all files in the specified folder.
	 * @param wosDBFolder The folder that contains all of the WoS files. The files must have the extension ".txt".
	 * @return
	 * @throws IOException
	 */
	public Set<Reference> readRefsFromWoSFolder(File wosDBFolder) throws IOException {

		// Get all references from DB folder. Files in that folder must have WoS
		// format
		// and must be of the form "*.txt"
		LOGGER.info("Opening folder " + wosDBFolder.getCanonicalPath());
		if (wosDBFolder.exists()) {
			Set<Reference> refs = new TreeSet<Reference>();
			for (File f : wosDBFolder.listFiles(wosFilesFilter)) {
				refs.addAll(readRefsFromWoSFile(f));
				LOGGER.info("Reading file: " + f.getName());
			}
			return refs;
		} else {
			throw new FileNotFoundException(wosDBFolder.getAbsolutePath() + " does not exist.");
		}
	}

	/** Reads a set of bibliographic references from a file containing a list of references. This file must not
	 * be in WoS format, but list all of the references sequentially. 
	 * @param refsFile
	 * @return
	 * @throws FileNotFoundException
	 */
	public Set<Reference> readRefsFromListFile(File refsFile) throws FileNotFoundException {
		Scanner s = new Scanner(refsFile);
		Set<Reference> refs = new LinkedHashSet<Reference>();
		while (s.hasNextLine()) {
			String line = s.nextLine();

			try {
				refs.add(new Reference(line));
			} catch (RuntimeException e) {
				LOGGER.info(e.toString());
			} catch (Exception e) {
				LOGGER.info(e.toString());
			}
		}
		return refs;

	}


	public <T extends TupleOfStrings> void writeTuplesOfStringsToList(Collection<T> tuples, PrintWriter w, Comparator<T> comparator) throws FileNotFoundException {
		Collection<T> tuplesToWrite;
		if (comparator != null) {
			tuplesToWrite = new TreeSet<T>(comparator);
			tuplesToWrite.addAll(tuples);
		} else {
			tuplesToWrite = tuples;
		}
		for (T t : tuplesToWrite) {
			w.println(t.getUnprocessedString());
		}
		w.flush();
		w.close();
	}

	/**
	 * For each WoS file in {@code srcFolder}, replaces all of the references, as specified by {@code replacements}. The
	 * results are stored in {@code destFolder}  
	 * 
	 * @see #transformRefs(File, File, Map)
	 * @param srcFolder The folder containing the WoS files to process
	 * @param destFolder The folder where to store the results of the replacements
	 * @param replacements A map whose keys represent the references to find in the WoS files. The values mapped from those
	 * keys are the replacement text.
	 * TODO Add unit tests
	 * @throws Exception
	 */
	public void replaceRefsOfFolder(File srcFolder, File destFolder, final Map<Reference, String> replacements) throws Exception {
		Utils.deleteFilesInFolder(destFolder);

		// Replaces references
		// final Map<String, String> replacements =
		// ac.getReplacementsFromClusters(referenceClusters);
		TextTransformer referenceTransformer = new TextTransformer() {

			@Override
			public String transform(String text) throws TextTransformException {
				try {
					Reference r = new Reference(text);
					String rep = replacements.get(r);
					if (rep == null) {
						return r.getString();
					} else {
						return rep;
					}
				} catch (Exception e) {
					throw new TextTransformException(e);
				}
			}
		};

		replaceRefsOfFolder(srcFolder, destFolder, referenceTransformer);
	}

	/**
	 * For each WoS file in {@code srcFolder}, replaces all of the authors, as specified by {@code replacements}. The
	 * results are stored in {@code destFolder}  
	 * 
	 * @see #transformRefs(File, File, Map)
	 * @param srcFolder The folder containing the WoS files to process
	 * @param destFolder The folder where to store the results of the replacements
	 * @param replacements A map whose keys represent the authors to find in the WoS files. The values mapped from those
	 * keys are the replacement text.
	 * @throws Exception
	 * TODO Add unit tests
	 */
	/**
	 * @param srcFolder
	 * @param destFolder
	 * @param replacements
	 * @throws Exception
	 */
	public void replaceAuthorsOfReferencesOfFolder(File srcFolder, File destFolder, final Map<Author, String> replacements) throws Exception {

		TextTransformer authorTransformer = new TextTransformer() {

			@Override
			public String transform(String text) throws TextTransformException {
				try {
					Reference r = new Reference(text);
					String author = r.getAuthor();
					String authorReplacement = replacements.get(new Author(author));
					if (authorReplacement != null) {
						r.setAuthor(authorReplacement);
						return r.getString();
					} else {
						return text;
					}

				} catch (Exception e) {
					throw new TextTransformException(e);
				}
			}
		};
		replaceRefsOfFolder(destFolder, destFolder, authorTransformer);

	}
	

	public void replaceAuthorsOfBiblioRecordsOfFolder(File srcFolder, File destFolder, Map<Author, String> authorReplacements) throws FileNotFoundException {
		destFolder.mkdirs();

		for (File srcFile : srcFolder.listFiles(wosFilesFilter)) {
			File tmpFile = new File(destFolder, srcFile.getName() + TMP_EXTENSION);

			if (tmpFile.exists()) {
				tmpFile.delete();
			}
			PrintWriter w = new PrintWriter(tmpFile);

			Scanner s = new Scanner(srcFile);
			Iterator<BiblioRecord> it = BiblioRecord.getIterator(s);
			while (it.hasNext()) {
				
				BiblioRecord br = it.next();
				Set<String> changedAuthors = new LinkedHashSet<String>();
				for (String author : br.getField(BiblioRecord.AUTHORS)) {
					try {
						String replacement = authorReplacements.get(new Author(author));
						if (replacement == null) {
							changedAuthors.add(author);
						} else {
							if (Author.isFormatFirstUpperCaseWithCommas(author)) {
								changedAuthors.add(new Author(replacement).formatFirstUpperCaseWithCommas());
							} else {
								changedAuthors.add(new Author(replacement).formatUpperCaseNoCommas());
							}
						}
					} catch (ParseException e) {
						e.printStackTrace();
						changedAuthors.add(author);
					}
				}
				br.setField(BiblioRecord.AUTHORS, changedAuthors);
				br.write(w);
			}
			s.close();
			w.close();
			// Renames the temporary file to the destination file
			File dstFile = new File(destFolder, srcFile.getName());
			if (dstFile.exists()) {
				dstFile.delete();
			}
			tmpFile.renameTo(dstFile);

			tmpFile = new File(destFolder, srcFile.getName() + TMP_EXTENSION); // For some reason the
																				// tmp file is still
																				// there after renaming
			if (tmpFile.exists()) {
				tmpFile.delete();
			}
		}
		
	}
	
	
	

	/**
	 * Transforms a file using the specified {@link TextTransformer}. The results are stored in {@code destFolder} 
	 * @param srcFile
	 * @param destFolder
	 * @param transformer The instance of {@link TextTransformer} that indicates the way to transform the text 
	 * in each file of {@code srcFile)
	 * @throws FileNotFoundException
	 */
	private void transformRefs(File srcFile, File destFolder, TextTransformer transformer) throws FileNotFoundException {
		Scanner s = new Scanner(srcFile);
		destFolder.mkdirs();
		File tmpFile = new File(destFolder, srcFile.getName() + TMP_EXTENSION);

		PrintWriter w;
		if (tmpFile.exists()) {
			tmpFile.delete();
		}
		w = new PrintWriter(tmpFile);

		boolean readingRefs = false;

		while (s.hasNextLine()) {

			// Reads the line from the source file
			String line = s.nextLine();

			// Concatenates the DOI if it is in the next line
			if (line.trim().endsWith("DOI")) {
				line = line + " " + s.nextLine().substring(3);
			}

			// Verifies if the line corresponds to a bibliographic reference
			// This happens either if the line starts with "CR" or if it is
			// among the following lines of a line that starts with CR
			try {
				if (line.startsWith("CR ")) {
					readingRefs = true;
					// line = "CR " + replace(line.substring(3), replacements);
					line = "CR " + transformer.transform(line.substring(3));// replace(new
																			// Reference(line.substring(3)),
																			// replacements);
				} else if (readingRefs && line.startsWith(" ")) {
					line = line.substring(0, 3) + transformer.transform(line.substring(3)); // +
																							// replace(new
																							// Reference(line.substring(3)),
																							// replacements);
				} else { // In this case the line is not a biblio reference
					readingRefs = false;
				}
			} catch (TextTransformException e) {
				// LOGGER.error(line + "\n\t" + e);
				// do nothing
			}

			// write the destination file
			w.println(line);
		}
		s.close();
		w.close();
		// Renames the temporary file to the destination file
		// dstFile = new File(destFolder, srcFile.getName());
		File dstFile = new File(destFolder, srcFile.getName());
		if (dstFile.exists()) {
			dstFile.delete();
		}
		tmpFile.renameTo(dstFile);

		tmpFile = new File(destFolder, srcFile.getName() + TMP_EXTENSION); // For some reason the
																			// tmp file is still
																			// there after renaming
		if (tmpFile.exists()) {
			tmpFile.delete();
		}
	}

	/**
	 * Transforms all of the the references of all WoS files in a folder using a {@link TextTransformer}. The results
	 * are stored in {@code destFolder}
	 * 
	 * @see ReferenceManager#transformRefs(File, File, Map)
	 * @param srcFolder
	 * @param destFolder
	 * @param textTransformer
	 * @throws FileNotFoundException
	 */
	private void replaceRefsOfFolder(File srcFolder, File destFolder, TextTransformer textTransformer) throws FileNotFoundException {
		LOGGER.info("Writing references to " + destFolder);

		for (File f : srcFolder.listFiles(wosFilesFilter)) {
			transformRefs(f, destFolder, textTransformer);
		}

	}

	/**
	 * Creates a "super reference", i.e., a reference having the longest strings for each field
	 * of a list of references. 
	 * 
	 * @param refs The set of {@link Reference} used to construct the "super reference"
	 * @return
	 */
	public Reference createSuperReference(Set<Reference> refs) {

		Comparator<MultiCounter.ItemCounter<String>> c = new Comparator<MultiCounter.ItemCounter<String>>() {

			@Override
			public int compare(MultiCounter.ItemCounter<String> o1, MultiCounter.ItemCounter<String> o2) {
				int cmp = -o1.count.compareTo(o2.count);

				if (cmp == 0) {
					cmp = -o1.element.compareTo(o2.element);
				}
				return cmp;
			}
		};

		MultiCounter<String> authorCount = new MultiCounter<String>(c, false);
		MultiCounter<String> yearCount = new MultiCounter<String>(c, false);
		MultiCounter<String> journalCount = new MultiCounter<String>(c, false);
		MultiCounter<String> volCount = new MultiCounter<String>(c, false);
		MultiCounter<String> pageCount = new MultiCounter<String>(c, false);
		MultiCounter<String> doiCount = new MultiCounter<String>(c, false);

		for (Reference ref : refs) {
			String author = StringUtils.trimToNull(ref.getAuthor());
			String year = StringUtils.trimToNull(ref.getYear());
			String journal = StringUtils.trimToNull(ref.getJournal());
			String volume = StringUtils.trimToNull(ref.getVolume());
			String page = StringUtils.trimToNull(ref.getPage());
			String doi = StringUtils.trimToNull(ref.getDoi());

			authorCount.add(author);
			yearCount.add(year);
			journalCount.add(journal);
			volCount.add(volume);
			pageCount.add(page);
			doiCount.add(doi);

		}

		String author = authorCount.isEmpty() ? null : StringUtils.trimToNull(authorCount.orderedCountersIterator().next().element);
		String year = yearCount.isEmpty() ? null : StringUtils.trimToNull(yearCount.orderedCountersIterator().next().element);
		String journal = journalCount.isEmpty() ? null : StringUtils.trimToNull(journalCount.orderedCountersIterator().next().element);
		String volume = volCount.isEmpty() ? null : StringUtils.trimToNull(volCount.orderedCountersIterator().next().element);
		String page = pageCount.isEmpty() ? null : StringUtils.trimToNull(pageCount.orderedCountersIterator().next().element);
		String doi = doiCount.isEmpty() ? null : StringUtils.trimToNull(doiCount.orderedCountersIterator().next().element);

		return new Reference(author, year, journal, volume, page, doi);

	}

	/** Classifies a set of references by year.
	 * @param refs The set of references to process.
	 * @return An {@link OrderedMultiMap} where each key is a year and the values associated to that key 
	 * are the references corresponding to that year, e.g.:<br><br>
	 * <pre>
	 * 		1990 -> [ref1, ref2, ... , refn]
	 * 		1991 -> [refa, refb, ... , refm]
	 * 		...
	 * </pre>
	 */
	public OrderedMultiMap<String, Reference> getRefsPerYear(Collection<Reference> refs) {
		OrderedMultiMap<String, Reference> refsPerYear = new OrderedMultiMap<String, Reference>();

		for (Reference r : refs) {
			if (r.getYear() != null) {
				refsPerYear.add(r.getYear(), r);
			}
		}
		return refsPerYear;
	}



}
